#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <assert.h>
#include <errno.h>
#include <signal.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/dir.h>
#include <sys/stat.h>
#include <sys/time.h>

#include "bstrlib.h"
#include "server.h"
#include "common.h"
#include "receive.h"
#include "send_file_listing.h"
#include "send_metadata.h"
#include "send_slice.h"
#include "fd_list.h"

int finish_var = 0;

void finish(int i){
    finish_var = 1;
}

int setup_connection(void){
    /*Funcion utilizada para crear el socket y dejarlo escuchando en
    el puerto*/
    int fd_server = 0, yes=1;
    struct sockaddr_in server;

    /*Creamos el socket con protocolo de internet IPv4, de tipo stream
    (confiable)*/
    fd_server = socket(PF_INET, SOCK_STREAM, 0);
    if (fd_server == -1){
        perror("socket()");
        exit(1);
    }

    /*seteamos los valores de puerto y direccion del server utilizando
    funciones para convertir los ordenes de bytes, aumentando la portabilidad
    a otros sistemas*/

    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    /*INADDR_ANY se usa para autodetectar la direccion del servidor*/
    server.sin_addr.s_addr = htonl(INADDR_ANY); 

    /*Seteamos el socket para permitir que el puerto sea reusado*/
    if (setsockopt(fd_server, SOL_SOCKET, SO_REUSEADDR,\
        &yes, sizeof(int)) == -1){
        perror("setsockopt");
        exit(1);
    }

    /*Bindeamos el socket con el puerto y direccion en el servidor*/
    if(bind(fd_server, (struct sockaddr*)&server,\
       sizeof(struct sockaddr)) == -1){
        perror("bind()");
        exit(1);
    }

    /*Empezamos a escuchar conexiones entrantes, vamos a atender un maximo de
    Q_CONEX conexiones*/
    if(listen(fd_server, Q_CONEX) == -1) {
        perror("listen()");
        exit(1);
    }

    printf("Servidor corriendo...\n");

    return fd_server;
}

int listen_and_accept_connection(int fd_server){
    int aux_size = 0, fd_client = 0;
    char *aux_inet = NULL;
    struct sockaddr_in client;


    assert(fd_server != 0);
    aux_size = sizeof(struct sockaddr);
    /*Aceptamos la conexion entrante*/
    fd_client = accept(fd_server, (struct sockaddr *)&client,\
                (socklen_t *)&aux_size);
    if (fd_client < 0) {
        perror("accept()");
        finish(1);
    }
    aux_inet = inet_ntoa(client.sin_addr);
    printf("Se obtuvo una conexion desde %s\n", aux_inet);
    return fd_client;

}

int main(void){
    int fd_server=0, fd_client=0, i=0, j=0, maxDescriptor=0, recvd=0;
    fd_set read_fds, write_fds;
    fd_list list = NULL;

    signal(SIGPIPE,SIG_IGN);

    signal(SIGHUP, finish);
    signal(SIGINT, finish);
    signal(SIGTERM, finish);

    /*Creamos nuestra lista de file descriptors*/
    list = fd_list_create();
    /*Seteamos lo necesario para quedarnos escuchando en el puerto*/
    fd_server = setup_connection();

    /*cargamos el valor del fd_server como el descriptor mas grande*/
    maxDescriptor = fd_server;

    /*Bucle que mantenemos infinitamente para aceptar todas las conexiones
    entrantes*/

    while(!finish_var){
        /*eliminamos lo que hay en cada estructura*/
        FD_ZERO(&read_fds);
        FD_ZERO(&write_fds);
        FD_SET(fd_server, &read_fds);

        /*Aqui nos fijamos que fd's nos enviaron pedido de quit o
        cerraron su conexion repentinamente, removiendolos de nuestra
        lista*/
        for (j = 0; j < (fd_list_size(list)); j++){
            if(fd_list_get_cmd(list, j) == 3){
                /*Si el comando fue el 3, entonces el cliente nos ha
                pedido salir, a lo que respondemos, cerramos y removemos
                de la lista*/
                send_msg(fd_list_get_fd(list, j), MSG0);
                close(fd_list_get_fd(list, j));
                fd_list_remove(list, j);
            }
        }

        /*dentro de este for metemos en la estructura read_fds los fd
        de los que esperamos el cliente nos envie cosas, y en la
        estructura write_fds metemos a los que nos faltan datos para enviar*/
        for (j = 0; j < (fd_list_size(list)); j++){
            if(fd_list_get_finish(list, j)){
                /*si ya terminamos de atender el comando anterior
                entonces vamos a quedarnos a la escucha de un comando
                nuevo*/
                FD_SET(fd_list_get_fd(list, j), &read_fds);
            }
            else{
                /*si no terminamos de atender, lo ponemos en la
                estructura de "para escribir"*/
                FD_SET(fd_list_get_fd(list, j), &write_fds);
            }
        }
        if (select(maxDescriptor + 1, &read_fds, &write_fds, NULL, NULL) == -1){
            perror("select()");
            printf("Cerrando Servidor...\n");
            close(fd_server);
            fd_list_destroy(list);
            list=NULL;
            exit(1);
        }
        /*Recorremos todos los file descriptors en busca de quienes
        esperan algo, o quieren enviar algo*/
        for (i = 0; i <= maxDescriptor; i++){
            /*si entramos en este if es porque llego algo de un cliente*/
            if (FD_ISSET(i, &read_fds)){
                /*Recibimos un intento de conexion*/
                if (i == fd_server){
                    /*tomamos la nueva conexion y la cargamos en nuestra
                    lista*/
                    fd_client = listen_and_accept_connection(fd_server);
                    if (fd_client > maxDescriptor){
                        maxDescriptor = fd_client;
                    }
                    fd_list_add_fd(list, fd_client);
                }
                /*Si no entro al if anterior, quiere decir que quizas
                algun cliente nos esta pidiendo algo, con lo cual
                usamos la siguiente funcion para ver si el fd se encuentra
                en nuestra lista, caso contrario obtendremos un -1*/
                else{
                    j = fd_list_search(list, i);
                    /*Si el fd se encuentra en la lista, entonces un cliente
                    nos ha hecho un pedido, y tenemos en la posicion J de la
                    estructura, toda la informacion necesaria*/
                    if (j != -1){
                        /*Atendemos la peticion del cliente*/
                        receive_info(list, j);
                    }
                }
            }
            /*Si no entro en el if anterior, es probable que haya un cliente
            esperando nuestra respuesta*/
            else if (FD_ISSET(i, &write_fds)){
                /*Intentamos buscar si el fd que espera datos se encuentra en
                nuestra lista*/
                j = fd_list_search(list, i);
                if (j != -1){
                    /*obtenemos el comando pedido por el cliente*/
                    recvd = fd_list_get_cmd(list, j);
                    if (recvd == 0)
                        /*Enviamos el listado de archivos*/
                        send_file_listing(list, j);
                    else if (recvd == 1)
                        /*Enviamos la metadata del archivo pedido*/
                        send_metadata(list, j);
                    else if (recvd == 2)
                        /*Enviamos la porcion del archivo pedido*/
                        send_slice(list, j);
                }
            }
        }
    }

    printf("Cerrando Servidor...\n");
    close(fd_server);
    fd_list_destroy(list);
    list=NULL;
    exit(1);
}
